package application.visual;

import info.clearthought.layout.TableLayout;
import io.IOClient;
import io.SerialCommunicator;

import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;

import model.Manipulator;
import model.ManipulatorListener;
import api.Command;
import api.Interpreter;
import api.Program;
import api.ProgramImplementor;
import api.ProgramState;
import api.error.InvalidRequestException;

public class MainPanel extends JPanel{

	private JLabel errorLabel;
	private JList commandList;
	private JList commList;
	private JTextPane output;
	private JTextField input;
	private JButton clearResponseButton;
	private List<Manipulator> manips;
	private JTabbedPane tabs;
	private Stack<String> executionStack;
	private int stackIndex = 0;
	private static PanelImplementor panelImp;
	
	public MainPanel()
	{
		super();
		initGUI();
	}
	private Stack<String> getStack()
	{
		if(executionStack == null)
		{
			executionStack = new Stack<String>();
		}
		return executionStack;
	}
	private JTabbedPane getTabbedPane()
	{
		if(tabs == null)
		{
			tabs = new JTabbedPane();
			tabs.addTab("All", getOutput());
		}
		return tabs;
	}
	private JTextPane getOutput()
	{
		if(output == null)
		{
			output = getNewOutput();
		}
		
		return output;
	}
	private JTextPane getNewOutput()
	{
		StyleContext sc = new StyleContext();
	    DefaultStyledDocument doc = new DefaultStyledDocument(sc);
		JTextPane output = new JTextPane(doc);
		output.addStyle("bold", null);
		output.getStyle("bold").addAttribute(StyleConstants.Bold, new Boolean(true));
		output.setFocusable(false);
		
		return output;
	}
	public Manipulator addConnection(String variable, IOClient client, Interpreter i)
	{
		for(Manipulator m : getManipulators())
		{
			if(m.getIO().getName().equals(client.getName()))
			{
				if(i.getClass().equals(m.getInterpreter().getClass()))
				{
					if(variable.equals(m.getVariableName()))
						return m;
				}
				else
				{
					int res = JOptionPane.showConfirmDialog(this, "Another type of connection has already " +
							"been created on " + client.getName() + ". Do you want to remove this connection and " +
							"to create the new one?");
					if(res == JOptionPane.NO_OPTION)
					{
						return null;
					}
					else
					{
						removeConnection(m.getIO());
						break;
					}
				}
			}
		}
		Manipulator m = new Manipulator(client, i);
		m.setVariableName(variable);
		ManipListener listen = new ManipListener(getNewOutput());
		m.addListener(listen);
		getManipulators().add(m);
		refreshListData();
		getTabbedPane().addTab(m.getVariableName(), listen.output);
		return m;
	}
	public void setError(String text)
	{
		getErrorLabel().setText(text);
	}
	public void executeProgram(File f)
	{
		try{
			Program p = new Program(f, panelImp = new PanelImplementor());
			p.execute();
		}catch(Exception e)
		{
			getErrorLabel().setText(e.getMessage());
		}
	}
	public ProgramState execute(String text)
	{
		
		getStack().push(text);
		stackIndex = -1;
		String[] splitter = text.split("\\.");
		if(splitter.length == 0){
			setError("Connection Not Found");
			return null;
		}
		else
			setError("");
		
		String var = splitter[0];
		var = var.trim();
		
		boolean isSetOfBots = isSetOfClients(var);
		boolean isArrayOfBots = isArrayOfClients(var);
		
		if(!isSetOfBots && !isArrayOfBots && !variableNameIsUsed(var)){
			getErrorLabel().setText("No connection with a variable name of " +
					"\"" + var + "\" could be found.");
			return null;
		}
		
		

			
		String data = "";
		for(int i = 1; i < splitter.length; i++)
			data += splitter[i] + ".";
		
		data = data.substring(0, data.length() - 1);
		while(data.length() > 0 && Character.isWhitespace(data.charAt(0)))
		{
			data = data.substring(1);
		}
		
		ProgramState state = new ProgramState();
		if(isSetOfBots)
		{
			for(Manipulator m : getManipulatorsByInterpreter(var))
			{
				try{
				m.sendCommand(data);
				state.addManipulator(m);
				}catch(InvalidRequestException er){
					getErrorLabel().setText(er.getMessage());
				}
			}
		}
		else if(isArrayOfBots)
		{
			List<Manipulator> vars = getManipulatorsByArray(var);
			Set<String> interpreterTypes = new TreeSet<String>();
			for(Manipulator m : vars)
			{
				interpreterTypes.add(m.getInterpreter().toString());
			}
			boolean contin = true;
			
			if(interpreterTypes.size() > 0)
			{
				String st = "There are more than one type of interpreter represented. Execute Command?\n";
				for(String s : interpreterTypes)
					st += s + "\n";
				
				contin = JOptionPane.showConfirmDialog(this, st) == JOptionPane.YES_OPTION;
			}
			if(contin)
			{
				for(Manipulator m : getManipulatorsByArray(var))
				{
					try {
						m.sendCommand(data);
						state.addManipulator(m);
					} catch (InvalidRequestException e) {
						getErrorLabel().setText(e.getMessage());
						break;
					}
				}
			}
		}
		else
		{
			for(Manipulator m : getManipulators())
			{
				if(m.getVariableName().equalsIgnoreCase(var))
				{
					try {
						m.sendCommand(data);
						state.addManipulator(m);
						break;
					} catch (InvalidRequestException e) {
						getErrorLabel().setText(e.getMessage());
						break;
					}
				}
			}
		}
		try{
		getOutput().getDocument().insertString(0, text + "\n", getOutput().getStyle("bold"));
		}catch(Exception e){}
		
		getInput().setText("");
		getCommandList().setListData(new Object[]{});
		
		repaint();
		
		return state;
	}
	private void refreshListData()
	{
		getCommList().setListData(getManipulators().toArray());
	}
	public void removeConnection(IOClient cl)
	{
		for(final Manipulator m : getManipulators())
		{
			if(m.getIO().equals(cl))
			{
				getManipulators().remove(m);
				m.disconnect();
				SwingUtilities.invokeLater(new Runnable(){
					public void run(){
						refreshListData();
						for(int i = 0; i < getTabbedPane().getTabCount(); i++)
						{
							if(getTabbedPane().getTitleAt(i).equals(m.getVariableName()))
							{
								getTabbedPane().removeTabAt(i);
								break;
							}
						}
					}
				});
				return;
			}
		}
	}
	protected List<Manipulator> getManipulators()
	{
		if(manips == null)
			manips = new ArrayList<Manipulator>();
		return manips;
	}
	protected List<IOClient> getConnectedClients()
	{
		List<IOClient> cl = new ArrayList<IOClient>();
		for(Manipulator m : getManipulators())
			cl.add(m.getIO());
		return cl;
	}
	
	protected boolean variableNameIsUsed(String var)
	{
		for(Manipulator m : getManipulators())
			if(m.getVariableName().equalsIgnoreCase(var))
				return true;
		return false;
	}
	private boolean isSetOfClients(String name)
	{
		for(Manipulator m : getManipulators())
			if(m.getInterpreter().toString().equals(name))
				return true;
		return false;
	}
	private boolean isArrayOfClients(String var)
	{
		var = var.replaceAll(" ", "");
		if(var.startsWith("[") && var.endsWith("]"))
		{
			String sub = var.substring(1,var.length() - 1);
			String[] vars = sub.split(",");
			String builder = "[";
			for(String s : vars)
				builder += s + ",";
			builder = builder.substring(0, builder.length()-1);
			builder += "]";
			return builder.equals(var);
		}
		else
			return false;
	}
	private List<Manipulator> getManipulatorsByInterpreter(String name)
	{
		List<Manipulator> subset = new LinkedList<Manipulator>();
		for(Manipulator m : getManipulators())
			if(m.getInterpreter().toString().equals(name))
				subset.add(m);
		return subset;
	}
	private List<Manipulator> getManipulatorsByArray(String arr)
	{
		arr = arr.replaceAll(" ", "");
		if(arr.startsWith("[") && arr.endsWith("]"))
		{
			String sub = arr.substring(1,arr.length() - 1);
			String[] vars = sub.split(",");
			List<Manipulator> list = new ArrayList<Manipulator>();
			
			for(String s : vars)
			{
				for(Manipulator m : getManipulators())
				{
					if(m.getVariableName().equals(s))
					{
						list.add(m);
						break;
					}
				}
			}
			return list;
		}
		else
			return new LinkedList<Manipulator>();
	}
	private JTextField getInput()
	{
		if(input == null)
		{
			input = new JTextField();
			input.addKeyListener(new KeyAdapter(){
				
				public void keyTyped(KeyEvent e) {
					if(e.getKeyChar() == '.')
					{
						String varname = getInput().getText();
						Manipulator man = null;
						boolean isSet = false;
						List<Manipulator> ms = ((isSet = isSetOfClients(varname))?getManipulatorsByInterpreter(varname):getManipulators());
						
						if(isSet)
						{
							man = ms.size() > 0? ms.get(0):null;
						}
						else
						{
							for(Manipulator m : getManipulators())
							{
								if(m.getVariableName().equalsIgnoreCase(varname))
								{
									man = m;
									break;
								}
							}
						}
						if(man == null)
							return;
						
						getCommandList().setListData(man.getInterpreter().getCommands());
					}
					
				}
				
				public void keyReleased(KeyEvent e) {
					
					if(e.getKeyCode() == 38)//up arrow
					{
						if(getStack().size() > stackIndex + 1)
						{
							stackIndex++;
							String s = getStack().get(getStack().size()-1-stackIndex);
							getInput().setText(s);
						}
					}
					else if(e.getKeyCode() == 40)//down arrow
					{
						if(stackIndex > 0)
						{
							stackIndex--;
							String s = getStack().get(getStack().size()-1-stackIndex);
							getInput().setText(s);
						}
						else if(stackIndex == 0)
						{
							getInput().setText("");
							stackIndex--;
						}
					}
				}
			});
			input.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
			input.setFont(new Font("Sans Serif", Font.BOLD, 16));
			
			
			input.addActionListener(new ActionListener(){
				public void actionPerformed(ActionEvent e)
				{
					String text = getInput().getText();
					execute(text);
				}
			});
		}
		
		return input;
	}
	private JLabel getErrorLabel()
	{
		if(errorLabel == null)
		{
			errorLabel = new JLabel();
			errorLabel.setForeground(Color.RED);
			
		}
		
		return errorLabel;
	}
	
	private JList getCommList()
	{
		if(commList == null)
		{
			commList = new JList();
			commList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

		}
		
		return commList;
	}
	private JList getCommandList()
	{
		if(commandList == null)
		{
			commandList = new JList();
			commandList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
			commandList.addListSelectionListener(new ListSelectionListener(){
				
				public void valueChanged(ListSelectionEvent e) {
					
					Command c = (Command)getCommandList().getSelectedValue();
					JOptionPane.showMessageDialog(MainPanel.this, c.getDescription());
				}
			});
		}
		
		return commandList;
	}
	
	private JButton getClearResponseButton()
	{
		if(clearResponseButton == null)
		{
			clearResponseButton = new JButton();
			clearResponseButton.setText("Clear Response");
			clearResponseButton.addActionListener(new ActionListener(){
				public void actionPerformed(ActionEvent e)
				{
					getOutput().setText("");
				}
			});
		}
		
		return clearResponseButton;
	}

	private void initGUI()
	{
		double[][] arr = new double[][]{
				{200, 600},
				{20, 30, 20, 300, 25, 80, 45}
		};
		
		TableLayout layout = new TableLayout(arr);
		layout.setHGap(5);
		layout.setVGap(5);
		
		setLayout(layout);
		
		add(new JLabel("Command Options"), "1,4,1,4,l,b");
		add(new JScrollPane(getCommandList()){
			{setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));}
		}, "1,5,1,5,f,f");
		
		add(new JLabel("Input"), "0,0,0,0,l,b");
		add(getInput(), "0, 1, 1, 1, f, f");
		
		add(new JLabel("Response"), "0,2,0,2,l,b");
		add(getTabbedPane(), "0,3,1,3,f,f");
		add(getClearResponseButton(), "1,4,1,4,r,t");
		
		add(new JLabel("Connected Controllers"), "0,4,0,4,l,b");
		add(new JScrollPane(getCommList()){
			{setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));}}, "0,5,0,5,f,f");
		
		add(getErrorLabel(), "1, 0, 1, 0, r, c");
	}
	
	
	private class PanelImplementor implements ProgramImplementor
	{

		@Override
		public Manipulator createConnection(String variable, String port,
				String interpreterClass) {
			
			Interpreter i = null;
			try{
				Class<?> c = Class.forName(interpreterClass);
				i  = (Interpreter)c.newInstance();
			}catch(Exception e){
				exception("Interpreter class: " + interpreterClass + " could not be found.");
				return null;
			}
			
			IOClient c = null;
			try{
				c = SerialCommunicator.getCommunicator(port);
			}catch(Exception e){
				exception("Unable to create a connection to " + port);
				return null;
			}
			
			return addConnection(variable, c, i);
		}

		@Override
		public void exception(String details) {
			System.out.println("EXCEPTION: " + details);
			getErrorLabel().setText(details);
			
		}

		@Override
		public boolean executeCommand(String command, int timeout) {
			ProgramState state = execute(command);
			if(state == null)
				return false;
			
			long startTime = System.currentTimeMillis();
			while((System.currentTimeMillis() - startTime) < timeout)
			{
				if(state.isReadyToContinue())
					return true;
				try{
					Thread.sleep(10);
				}catch(Exception e){}
			}
			return false;
		}

		@Override
		public void prompt(String text) {
			JOptionPane.showMessageDialog(MainPanel.this, text);
			
		}

		@Override
		public void releaseConnection(Manipulator manip) {
			removeConnection(manip.getIO());
			
		}

		@Override
		public String prompt(String question, Set<String> options) {
			Object[] optionArr = options.toArray();
			int res = JOptionPane.showOptionDialog(MainPanel.this, question, "USM Robotics", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, optionArr, optionArr[0]);
			if(res >= 0)
				return optionArr[res].toString();
			else
				return null;
		}
		
	}
	
	
	private class ManipListener implements ManipulatorListener
	{
		private JTextPane output;
		
		
		public ManipListener(JTextPane output)
		{
			this.output = output;
			int r = (int)(Math.random()*200);
			int b = (int)(Math.random()*150);
			int g = (int)(Math.random()*100);
			this.output.addStyle("color", null);
			this.output.getStyle("color").addAttribute(StyleConstants.ColorConstants.Foreground, new Color(r,g,b));
			this.output.getStyle("color").addAttribute(StyleConstants.Bold, new Boolean(true));
		}
		
		public void dataReady(Manipulator manip, List<String> data)
		{
				
			Calendar cal = Calendar.getInstance();
			cal.setTime(new Date());
			int second = cal.get(Calendar.SECOND);
			int minute = cal.get(Calendar.MINUTE);
			String time = cal.get(Calendar.HOUR) + ":" + (minute < 10?"0" + minute:"" + minute) + ":" + (second < 10?"0" + second:"" + second);
				
			for(String returnValue : data)
			{
				try{
					output.getDocument().insertString(0, returnValue + "\n", null);
					output.getDocument().insertString(0, time + " : ", getOutput().getStyle("bold"));
					getOutput().getDocument().insertString(0, returnValue + "\n", null);
					getOutput().getDocument().insertString(0, manip.getVariableName() + " @ " + time + " : ", output.getStyle("color"));
				}catch(BadLocationException ble){}	
				
			}
		}
		
		public void dataSending(Manipulator manip, String command) {
			try{
			output.getDocument().insertString(0, command + "\n", output.getStyle("bold"));
		
			}catch(Exception e){}
		}
	}
}
